# coding: latin-1
"""HeightMap reading, saving, rendering and handling.
"""
from __future__ import division
from operator import attrgetter

import math
import pygame
from vector import Vector, AngleVector
from pathing import PathingMap
from terrain import TERRAINS, Temperate

import OpenGL.GL as ogl
import OpenGL.GLU as oglu

from PIL import Image

import logging
LOAD_LOGGER = logging.getLogger('artillery.loading')

import unit

try:
	from psyco.classes import *
except ImportError:
	pass

INFO = set(("terrain", "scale", "tilesize"))
def read(mapfile):
	"""Read a heightmap's meta data for scale and terrain information, then
	use pixel values to determine vertex heights."""
	heightmap = Image.open(mapfile)
	pixels = heightmap.load()
	
	attributes = {}
	
	for attribute in INFO:
		try:
			attributes[attribute] = heightmap.info[attribute]
		except KeyError:
			LOAD_LOGGER.warning("Missing map data: %s" % attribute)
	
	width, height = heightmap.size
	return attributes, [[pixels[x, y][0] for x in xrange(width)] for y in xrange(height)]

def save(filename, heights):
	width, height = len(heights[0]), len(heights)
	
	heightmap = pygame.Surface((width, height), depth=24)
	
	for y, row in enumerate(heightmap):
		for x, height in enumerate(row):
			colour = (height,) * 3
			heightmap.set_at((x, y), colour)

class Vertex(Vector):
	def __init__(self, position, height):
		x, y = position
		super(Vertex, self).__init__((x, y, height))
		self.position = position
		self.height = height
		x, y = self.position
		z = self.height
	
	def __repr__(self):
		x, y = self.position
		return str((x, y, self.height))
	
	__str__ = __repr__

class Plane(object):
	def __init__(self, points):
		A = points[0]
		B = points[1]
		C = points[2]
		
		AB = B - A
		AC = C - A
		
		self.normal = -AB.cross(AC).normalised()
	
	def angle_to_plane(self, plane):
		return math.acos(-self.normal.dot(plane.normal))
	
	def angle_to_line(self, line):
		line = line.normalised()
		
		return math.acos(self.normal.dot(line))

class Triangle(Plane):
	def __init__(self, heightmap, vertices):# indices):
		super(Triangle, self).__init__(vertices)
		
		self.heightmap = heightmap
		self.vertices = vertices
		#self.indices = indices
		
		self.centre = self._centre()
				
		self.incline = abs(self.angle_to_plane(self.heightmap.FLOOR))
		self.colour, self.specular, self.emission = heightmap.terrain.surface(self.centre, self.incline)
	
	def _centre(self):
		average = self.vertices[0] + self.vertices[1]
		average += self.vertices[2]
		average /= 3
		
		return average
	
	def draw(self):
		ogl.glNormal3f(*self.normal.dimensions)
		ogl.glColor4f(*self.colour)
		ogl.glMaterialfv(ogl.GL_FRONT_AND_BACK, ogl.GL_SPECULAR, self.specular)
		ogl.glMaterialfv(ogl.GL_FRONT_AND_BACK, ogl.GL_EMISSION, self.emission)
		
		#glDrawElementsui(ogl.GL_TRIANGLES, self.indices)
		ogl.glVertex3f(*self.vertices[0])
		ogl.glVertex3f(*self.vertices[1])
		ogl.glVertex3f(*self.vertices[2])

class Segment(object):
	def __init__(self, start, end):
		self.start = start
		self.end = end
		self.length = (self.start - self.end).magnitude()
	
	def __repr__(self):
		return "Segment(%s, %s)" % (repr(self.start), repr(self.end))

class Side(Plane):
	def __init__(self, heightmap, vertices):
		super(Side, self).__init__([vertices[0], vertices[1], vertices[-1]])
		self.vertices = vertices
		#self.centre = self._centre()
	
	def _centre(self):
		count = len(self.vertices)
		
		return sum(self.vertices, Vector((0, 0, 0))) / count

	def draw(self):
		ogl.glNormal3f(*self.normal)
		
		ogl.glBegin(ogl.GL_QUAD_STRIP)	
		for vertex in self.vertices:
			ogl.glVertex3f(*vertex)
		ogl.glEnd()

def intersect(a, b):
	p1, p2 = a.start, a.end
	p3, p4 = b.start, b.end
	
	denom = (p4.y - p3.y)*(p2.x - p1.x) - (p4.x - p3.x)*(p2.y - p1.y)
	
	if denom == 0:
		return None
	
	p1p3x, p1p3y = p1.x - p3.x, p1.y - p3.y
	
	ua = (((p4.x - p3.x)*p1p3y - (p4.y - p3.y)*p1p3x) 
		/ denom)
	
	ub = (((p2.x - p1.x)*p1p3y - (p2.y - p1.y)*p1p3x) 
		/ denom)
	
	#if not 0 <= ua <= 1:
	#	return None
	if not 0 <= ub <= 1:
		return None
	
	x = p1.x + ua * (p2.x - p1.x)
	y = p1.y + ua * (p2.y - p1.y)
	
	return Vector((x, y, 0))

class HeightMap(object):
	FLOOR = Plane((Vertex((0, 1), 0), Vertex((1, 0), 0), Vertex((0, 0), 0)))
	SUN = Vector((320, 160, 640))
	TOPEYE = Vector((0, 0, 1)).normalised()
	
	def __init__(self, vertices, tilesize=16, terrain=Temperate, scale=8):
		LOAD_LOGGER.info("Reading height values")
		
		self.vertices = vertices
		self.width, self.height = len(self.vertices[0])-1, len(self.vertices)-1
		self.tilesize = tilesize
		
		LOAD_LOGGER.info("Initialising terrain")
		self.terrain = terrain()
		
		LOAD_LOGGER.info("Calculating MYSTERIOUS RATIO")
		# Dare you discover the power of the MYSTERIOUS RATIO?
		mysterious_ratio = 9.2/8
		
		LOAD_LOGGER.info("Generating vertices")
		self.vertices.reverse()
		factor = (mysterious_ratio * scale) / 15
		
		w, h = self.width / 2, self.height / 2
		for y, row in enumerate(self.vertices):
			for x, height in enumerate(row):
				row[x] = Vertex(((x-w) * tilesize, (y-h) * tilesize), height * factor)
		
		#LOAD_LOGGER.info("Calculating cell heights")
		self.unit_heights = [[0 for x in xrange(self.width)] 
											for y in xrange(self.height)]
		
		total_tris = self.width * self.height * 2
		if total_tris > 20000:
			LOAD_LOGGER.warning("Attempting to load larger than recommended map.")
		
		LOAD_LOGGER.info("Generating triangles (%s)" % total_tris)
		self.inclines = [[(0, 0, 0, 0) for x in xrange(self.width)] 
										for y in xrange(self.height)]
		self.triangles, self.segments = self._generate_triangles_segments()
		
		self.pathing = PathingMap(self.unit_heights, self.inclines)
		self.standable = self.pathing.standable
		self.walkable = self.pathing.walkable
		self.walk_time = self.pathing.walk_time
		
		LOAD_LOGGER.info("Generating sides")
		self.sides = self._generate_sides()
		
		LOAD_LOGGER.info("Leaving splines as God intended THANKYOUVERYMUCH")
		
		fakeunit = unit.Unit(self, (0, 0), (0, 0, 0))
		self.positions = [(x, y) for x in xrange(self.width-1) for y in xrange(self.height-1) if self.standable((x, y), fakeunit)]
		
		import random
		self.units = set()
		self.units |= set([unit.Unit(self, random.choice(self.positions), (1, 0, 0)) for x in xrange(3)])
		self.units |= set([unit.Unit(self, random.choice(self.positions), (0, 1, 0)) for x in xrange(3)])
		self.units |= set([unit.Unit(self, random.choice(self.positions), (0, 0, 1))])
		
		self.unitgrid = [[None for x in xrange(self.width)] 
								for y in xrange(self.height)]
		
		for unit_ in self.units:
			x, y = unit_.position
			
			self.unitgrid[y][x] = unit_
		
		self.list_offset = ogl.glGenLists(2)
		LOAD_LOGGER.info("Compiling geometry")
		self._compile_geometry()
		LOAD_LOGGER.info("Compiling render")
		self._compile_render()
	
	def _compile_geometry(self):
		ogl.glNewList(self.list_offset + 2, ogl.GL_COMPILE)
		ogl.glBegin(ogl.GL_TRIANGLES)
		for tri in self.triangles:
			tri.draw()
		ogl.glEnd()
		ogl.glEndList()
	
	def _compile_render(self):
		ogl.glNewList(self.list_offset + 1, ogl.GL_COMPILE)
		ogl.glColor4f(*self.terrain.side_colour)
		ogl.glMaterialfv(ogl.GL_FRONT_AND_BACK, ogl.GL_SPECULAR, self.terrain.side_specular)
		ogl.glMaterialfv(ogl.GL_FRONT_AND_BACK, ogl.GL_EMISSION, self.terrain.side_emission)
		for side in self.sides:
			side.draw()
		ogl.glCallList(self.list_offset + 2)
		ogl.glPushAttrib(ogl.GL_DEPTH_BUFFER_BIT | ogl.GL_LINE_BIT | ogl.GL_POLYGON_BIT | ogl.GL_COLOR_BUFFER_BIT)
		ogl.glLineWidth(2)
		ogl.glDepthFunc(ogl.GL_LEQUAL)
		ogl.glPolygonMode(ogl.GL_BACK, ogl.GL_LINE)
		ogl.glCullFace(ogl.GL_FRONT)
		ogl.glCallList(self.list_offset + 2)
		ogl.glPopAttrib()
		ogl.glEndList()
	
	def _generate_triangles_segments(self):
		triangles, segments = [], []
		
		def square(x, y):
			return (x, y), (self.vertices[y][x],     self.vertices[y+1][x], 
					self.vertices[y+1][x+1], self.vertices[y][x+1])
		
		cells = [square(x, y) for x in xrange(self.width)
								for y in xrange(self.height)]
		
		# Calculate grid segments for cross section calculation
		segments = [Segment(cell[0], cell[1]) for _, cell in cells] 
		segments += [Segment(cell[3], cell[0]) for _, cell in cells]
		segments += [Segment(cell[2], cell[3]) for _, cell in cells[(self.height-1) * self.width:]]
		#segments += [Segment(cells[((y+1) * self.width)-1][1], cells[((y+1) * self.width)-1][2]) for y in xrange(self.height)]
		
		def key(pair):
			return abs(pair[0].height - pair[1].height)
		
		angle = math.radians(35)
		for position, cell in cells:
			x, y = position
			diagonal1 = cell[0], cell[2]
			diagonal2 = cell[1], cell[3]
			
			if key(diagonal1) <= key(diagonal2):
				diagonal = diagonal1
				# 1__2
				# |1/|  ^  >  v  <
				# |/2| (1, 2, 2, 1)
				# 0‾‾3
				tri1 = Triangle(self, (cell[2], cell[1], cell[0]))
				tri2 = Triangle(self, (cell[0], cell[3], cell[2]))				
				segments.append(Segment(cell[0], cell[2]))
				
				self.inclines[y][x] = (tri1.incline, tri2.incline, 
										tri2.incline, tri1.incline)
			else:
				diagonal = diagonal2
				# 1__2
				# |\2|  ^  >  v  <
				# |1\| (2, 2, 1, 1)
				# 0‾‾3
				tri1 = Triangle(self, (cell[3], cell[1], cell[0]))
				tri2 = Triangle(self, (cell[3], cell[2], cell[1]))
				segments.append(Segment(cell[1], cell[3]))
				
				self.inclines[y][x] = (tri2.incline, tri2.incline, 
										tri1.incline, tri1.incline)
			
			if ((tri1.incline > angle and tri2.incline > angle)
				or (tri1.incline <= angle and tri2.incline <= angle)):
				self.unit_heights[y][x] = ((tri1.centre + tri2.centre) / 2).z
			else:
				self.unit_heights[y][x] = ((diagonal[0] + diagonal[1]) / 2).z
			
			triangles.extend([tri1, tri2])
		
		return triangles, segments
	
	def _generate_sides(self):
		s = self.tilesize
		x, y = (self.width)*(s/2), (self.height)*(s/2)
		def interleave(*threads):
			return sum((list(x) for x in zip(*threads)), [])
		
		floor = [Vertex((x - (x_ * s), -y), -1) for x_ in xrange(self.width + 1)]
		side = reversed(self.vertices[0])
		topside = Side(self, interleave(floor, side))
		
		floor = [Vertex((-x + (x_ * s), y), -1) for x_ in xrange(self.width + 1)]
		side = self.vertices[-1]
		bottomside = Side(self, interleave(floor, side))

		floor = [Vertex((-x, -y + (y_ * s)), -1) for y_ in xrange(self.height + 1)]
		side = [self.vertices[y_][0] for y_ in xrange(self.height + 1)]
		leftside = Side(self, interleave(floor, side))

		floor = [Vertex((x, y - (y_ * s)), -1) for y_ in xrange(self.height + 1)]
		side =  reversed([self.vertices[0][0]] + [self.vertices[y_][-1] for y_ in xrange(self.height + 1)])
		rightside = Side(self, interleave(floor, side))
		
		return [topside, bottomside, leftside, rightside]
	
	def walk_cost(self, position, direction):
		directions = (0, 1, 2, 3, 4, 5, 6, 7)
		moves = ((0, 1), (1, 0), (0, -1), (-1, 0), 
				(1, 1), (1, -1), (-1, -1), (-1, 1))
		costs = (1, 1.4142135623730951)
		rev = (2, 3, 0, 1)
		
		mx, my = moves[direction]
		ox, oy = position
		dx, dy = ox + mx, oy + my
		
		if True:# direction <= 3:
			angle = math.radians(35)
			
			#cost = ((self.inclines[oy][ox][direction] + self.inclines[dy][dx][rev[direction]]) / 2) + 1
			if self.unit_heights[oy][ox] <= self.unit_heights[dy][dx]:
				cost = costs[direction // 4] * (abs(self.unit_heights[oy][ox] - self.unit_heights[dy][dx])/8 + 1)
			else:
				cost = costs[direction // 4] * (abs(self.unit_heights[oy][ox] - self.unit_heights[dy][dx])/8)
		
			return cost
		"""else:
			straights = (0, 1, 2, 3, 0)
			dir1, dir2 = straights[direction-4], straights[direction-3]
			
			mx, my = moves[direction]
			ox, oy = position
			dx, dy = ox + mx, oy + my
			
			origin, dest = (ox, oy), (dx, dy)
			
			# This hax has sth to do with transforming the intermediate coords
			# in 90 degree angles or sth. (I worked it out by tables)
			sign = mx * my
			negative = sign < 0
			positive = sign > 0
			inter1 = ox + (negative * mx), oy + (positive * my)
			inter2 = ox + (positive * mx), oy + (negative * my)
			
			if self.walkable(origin, dir2) and self.walkable(inter2, dir1):
				return costs[1] * (self.walk_cost(origin, dir2) + self.walk_cost(inter2, dir1)) / 2
			else:
				return costs[1] * (self.walk_cost(origin, dir1) + self.walk_cost(inter1, dir2)) / 2"""
	
	def update(self):
		self.terrain.update()
	
	def pixel_size(self):
		return self.width * self.tilesize, self.height * self.tilesize
	
	def render(self, angle=0, zoom=1, viewshift=Vector((0, 0, 0)), axonometric=False):
		ogl.glMatrixMode(ogl.GL_PROJECTION)
		ogl.glLoadIdentity()
		ogl.glOrtho(-32 / zoom, 32 / zoom, -24 / zoom, 24 / zoom, -256 / zoom, 256 / zoom)
		
		ogl.glMatrixMode(ogl.GL_MODELVIEW)
		ogl.glLoadIdentity()
		
		x, y, z = viewshift
		#x, y, z = 0, 0, 0
		
		sin = math.sin(math.radians(angle))
		cos = math.cos(math.radians(angle))
		
		a = math.tan(math.radians(30))
		#a=0.577350269
		
		ogl.glPushAttrib(ogl.GL_DEPTH_BUFFER_BIT)
		if axonometric:
			abs_eye = Vector((cos, sin, a)) - (viewshift)
			oglu.gluLookAt(abs_eye[0], abs_eye[1], abs_eye[2], -viewshift[0], -viewshift[1], -viewshift[2], 0, 0, 1)
		else:
			#look = Vector((0, 0, 0))
			#eye = Vector((0, 0, 1))
			ogl.glDisable(ogl.GL_DEPTH_TEST)
			abs_eye = Vector((0, 0, 1)) - (viewshift)
			oglu.gluLookAt(abs_eye[0], abs_eye[1], abs_eye[2], -viewshift[0], -viewshift[1], -viewshift[2], -cos, -sin, 0)
		
		eye = cos, sin, a
		self.terrain.angle = angle
		self.terrain.update(eye)
		self.terrain.light_setup()
		
		ogl.glCallList(self.list_offset + 1)
		
		for unit in self.units:
			unit.render()
		
		ogl.glPopAttrib()
		
		#for unit in self.units:
		#	unit.render()
		
		"""x, y = 15, 7
		
		#x, y, z = viewshift
		position = x, y
		x, y = position
		
		def average(position):
			x, y = position
			v = self.vertices
			v1, v2, v3, v4 = v[y][x], v[y+1][x+1], v[y+1][x], v[y][x+1]
			average = (v1 + v2 + v3 + v4) / 4
			return average.z
		
		#v = self.vertices
		#v1, v2, v3, v4 = v[y][x], v[y+1][x+1], v[y+1][x], v[y][x+1]
		
		def square(x, y):
			return (self.vertices[y][x],     self.vertices[y+1][x+1], 
					self.vertices[y+1][x], self.vertices[y][x+1])
		
		zup = Vector((0, 0, 0.5))
		
		#v1 = v1 + zup
		#v2 = v2 + zup
		#v3 = v3 + zup
		#v4 = v4 + zup
		
		#squares = [square(x_, y_) for x_ in xrange(self.width) for y_ in xrange(self.height) if y_ + x_ < 10]
		
		squares = [square(x, y)] + [square(x+1, y), square(x, y-1), square(x-1, y), square(x, y+1)]
		
		ogl.glPushAttrib(ogl.GL_DEPTH_BUFFER_BIT)
		ogl.glDisable(ogl.GL_DEPTH_TEST)
		ogl.glColor3fv((1, 0, 0))
		for square in squares:
			v1, v2, v3, v4 = square
			ogl.glBegin(ogl.GL_LINE_LOOP)
			ogl.glVertex3f(*v1)
			ogl.glVertex3f(*v3)
			ogl.glVertex3f(*v2)
			ogl.glVertex3f(*v4)
			ogl.glEnd()
		ogl.glPopAttrib()"""
		
		"""glBegin(ogl.GL_LINE_STRIP)
		for h_ in heights:
			x, y = h_
			ogl.glVertex3f(x, y, 1) 
		ogl.glEnd()
		
		height = average(position)
		x, y = position
		w, h = self.width//2, self.height//2
		ogl.glTranslatef((x-w)*16 + 8, (y-h)*16 + 8, height)
		
		q = oglu.gluNewQuadric()
		ogl.glColor3fv((1, 1, 1))
		oglu.gluQuadricOrientation(q, GLU_INSIDE)
		ogl.glCullFace(ogl.GL_FRONT)
		ogl.glShadeModel(ogl.GL_SMOOTH)
		oglu.gluSphere(q, 8, 16, 16)
		ogl.glShadeModel(ogl.GL_FLAT)
		ogl.glCullFace(ogl.GL_BACK)"""
		
		"""w, h = self.width * 8, self.height * 8
		
		#LINE = Segment(Vector((-w, -h, 0)), Vector((-w, -h, 0)) + AngleVector(math.radians(angle+90)))
		LINE = Segment(Vector((0, 0, 0)), AngleVector(math.radians(angle+90)))
		#print LINE
		
		inters = []
		for segment in self.segments:
			position = intersect(LINE, segment)
		
			if position is not None:
				start, end = segment.start, segment.end
				
				position.z = start.z
				dist = (start - position).magnitude() / Vector((start.x-end.x, start.y-end.y, 0)).magnitude()
				
				height = start.z + dist*(end.z - start.z)
				position.z = height+1
				
				inters.append(position)
		
		l = max(w, h)
		
		def key(point):
			return (AngleVector(math.radians(angle+90), l*2) - point).magnitude()
		
		inters.sort(key=key, reverse=True)
		#print inters
		
		w = self.width // 2
		h = self.height // 2
		
		ogl.glColor3f(1, 0, 0)
		ogl.glPointSize(4)
		#glBegin(ogl.GL_POINTS)
		ogl.glBegin(ogl.GL_LINE_STRIP)
		for inter in inters:
			ogl.glVertex3f(*inter)
		ogl.glEnd()"""
		
		w = self.width / 2
		h = self.height / 2
		
		"""# DEBUG WALKABILITY
		ogl.glLineWidth(1) 
		ogl.glPushAttrib(ogl.GL_DEPTH_BUFFER_BIT)
		ogl.glDisable(ogl.GL_DEPTH_TEST)
		ogl.glBegin(ogl.GL_LINES)
		for y in xrange(self.height):
			for x in xrange(self.width):
				cx, cy = (x-w)*16 + 8, (y-h)*16 + 8
				if x < self.width-1:
					if self.walkable((x, y), 1):
						ogl.glColor3f(0, 0, 1)
						ogl.glVertex3f(cx, cy, self.unit_heights[y][x] + 1)
						ogl.glVertex3f(cx + 16, cy, self.unit_heights[y][x+1] + 1)
				if y < self.height-1:
					if self.walkable((x, y), 0):
						ogl.glColor3f(0, 0, 1)
						ogl.glVertex3f(cx, cy, self.unit_heights[y][x] + 1)
						ogl.glVertex3f(cx, cy+16, self.unit_heights[y+1][x] + 1)
					
					if x < self.width-1:
						if self.walkable((x, y), 4):
							ogl.glColor3f(0, 0, 1)
							ogl.glVertex3f(cx, cy, self.unit_heights[y][x] + 1)
							ogl.glVertex3f(cx+16, cy+16, self.unit_heights[y+1][x+1] + 1)
				if 0 < x < self.width:
					if y < self.height-1:
						if self.walkable((x, y), 7):
							ogl.glColor3f(0, 0, 1)
							ogl.glVertex3f(cx, cy, self.unit_heights[y][x] + 1)
							ogl.glVertex3f(cx-16, cy+16, self.unit_heights[y+1][x-1] + 1)
						
		ogl.glEnd()
		
		ogl.glColor3f(1, 1, 1)
		ogl.glPointSize(3)
		ogl.glBegin(ogl.GL_POINTS)
		for y in xrange(self.height):
			for x in xrange(self.width):
				if (x, y) in self.positions:
					cx, cy = (x-w)*16 + 8, (y-h)*16 + 8
					ogl.glVertex3f(cx, cy, self.unit_heights[y][x] + 1)
		ogl.glEnd()
		ogl.glPopAttrib()
		"""
